/* @flow */

import type VueRouter from './index'
import {resolvePath} from './util/path'
import {assert, warn} from './util/warn'
import {createRoute} from './util/route'
import {fillParams} from './util/params'
import {createRouteMap} from './create-route-map'
import {normalizeLocation} from './util/location'

export type Matcher = {
    match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;
    addRoutes: (routes: Array<RouteConfig>) => void;
};

export function createMatcher(routes: Array<RouteConfig>,
                              router: VueRouter): Matcher {
    //pathList：每个路由都存在一个path属性，该属性用于存储经过标准化处理的path的值，标准化的意思就是
    //      生成该router完整的绝对路径
    //pathMap：该变量是一个对象，其中键是经过标准化处理之后的path的值，值为RouteRecord类型的对象
    //nameMap：该变量是一个对象，其中键是路由中name属性的值，值为RouteRecord类型的对象
    const {pathList, pathMap, nameMap} = createRouteMap(routes)

    function addRoutes(routes) {
        createRouteMap(routes, pathList, pathMap, nameMap)
    }

    /**
     * 该方法用于根据row参数指定的跳转位置生成相应的Route
     * @param raw
     * @param currentRoute
     * @param redirectedFrom
     * @returns {Route}
     */
    function match(raw: RawLocation,
                   currentRoute?: Route,
                   redirectedFrom?: Location): Route {
        const location = normalizeLocation(raw, currentRoute, false, router)
        const {name} = location

        //name的优先级始终比path高
        if (name) {
            const record = nameMap[name]
            if (process.env.NODE_ENV !== 'production') {
                warn(record, `Route with name '${name}' does not exist`)
            }
            if (!record) return _createRoute(null, location)
            const paramNames = record.regex.keys
                .filter(key => !key.optional)
                .map(key => key.name)

            if (typeof location.params !== 'object') {
                location.params = {}
            }

            if (currentRoute && typeof currentRoute.params === 'object') {
                for (const key in currentRoute.params) {
                    //在location.params中不存在，但是在paramNames中存在
                    if (!(key in location.params) && paramNames.indexOf(key) > -1) {
                        location.params[key] = currentRoute.params[key]
                    }
                }
            }

            if (record) {
                location.path = fillParams(record.path, location.params, `named route "${name}"`)
                return _createRoute(record, location, redirectedFrom)
            }
        } else if (location.path) {
            location.params = {}
            for (let i = 0; i < pathList.length; i++) {
                const path = pathList[i]
                const record = pathMap[path]
                if (matchRoute(record.regex, location.path, location.params)) {
                    return _createRoute(record, location, redirectedFrom)
                }
            }
        }
        // no match
        return _createRoute(null, location)
    }

    //重定向，会改变url
    function redirect(record: RouteRecord,
                      location: Location): Route {
        const originalRedirect = record.redirect
        let redirect = typeof originalRedirect === 'function'
            ? originalRedirect(createRoute(record, location, null, router))
            : originalRedirect

        //如果为字符串类型，则表示是path
        if (typeof redirect === 'string') {
            redirect = {path: redirect}
        }

        if (!redirect || typeof redirect !== 'object') {
            if (process.env.NODE_ENV !== 'production') {
                warn(
                    false, `invalid redirect option: ${JSON.stringify(redirect)}`
                )
            }
            return _createRoute(null, location)
        }

        const re: Object = redirect
        const {name, path} = re
        let {query, hash, params} = location

        //可以为route的redirect配置query、hash、params等属性，
        //其实redirect属性的类型就是RawLocation或者返回RawLocation类型对象的方法，如果为redirect属性配置了
        //query、hash、params，则其优先级更高。就是可以为重定向的url设置query、hash和params
        query = re.hasOwnProperty('query') ? re.query : query
        hash = re.hasOwnProperty('hash') ? re.hash : hash
        params = re.hasOwnProperty('params') ? re.params : params

        if (name) {
            // resolved named direct
            const targetRecord = nameMap[name]
            if (process.env.NODE_ENV !== 'production') {
                assert(targetRecord, `redirect failed: named route "${name}" not found.`)
            }

            //match方法的第一个参数为redirect对应的将要跳转到的位置信息，第三个参数为从其位置跳转到第一个参数指定的位置。
            //示例：
            //{path:'/a', redirect:'/b'}
            //则第一个参数为'/b'对应生成的Location类型的对象，而第三个参数为'/a'对应的Location类型的对象
            return match({
                _normalized: true,
                name,
                query,
                hash,
                params
            },
                //为什么此处可以直接传递一个undefined？
                //match方法的第二个参数是currentRoute，该参数的作用有两个，一是normalizeLocation，但是此时无需normalize，
                //二是根据currentRoute的params属性更新location的params，但是在进入redirect方法之前，location的params属性
                //已经更新过了，所以无需再次更新。
                undefined,
                location)
        } else if (path) {
            // 1. resolve relative redirect
            //示例：
            /**
             * {
             *      path:'/a',
             *      children:{
             *          path:'b',
             *          redirect:{
             *              path:'c'
             *          }
             *      }
             * }
             * 首先需要了解，对于RouteRecord对象来说，其path属性是经过标准化后的，详情可查看
             * declarations.js中的注释，但是其redirect属性是未处理的。
             * 如上所示，一旦访问'/a/b'，就会被重定向到'c'，而resolveRecordPath方法就是修正，使最后的
             * rawPath为'/a/c'，如果为redirect:{path:'/c'}，则rawPath为'/c'
             */
            const rawPath = resolveRecordPath(path, record)
            // 2. resolve params
            const resolvedPath = fillParams(rawPath, params, `redirect route with path "${rawPath}"`)
            // 3. rematch with existing query and hash
            return match({
                _normalized: true,
                path: resolvedPath,
                query,
                hash
            }, undefined, location)
        } else {
            if (process.env.NODE_ENV !== 'production') {
                warn(false, `invalid redirect option: ${JSON.stringify(redirect)}`)
            }
            return _createRoute(null, location)
        }
    }

    //别名，相当于后台的转发，不会改变url
    function alias(record: RouteRecord,
                   location: Location,
                   matchAs: string): Route {
        const aliasedPath = fillParams(matchAs, location.params, `aliased route with path "${matchAs}"`)
        const aliasedMatch = match({
            _normalized: true,
            path: aliasedPath
        })
        if (aliasedMatch) {
            const matched = aliasedMatch.matched
            const aliasedRecord = matched[matched.length - 1]
            /**
             * 为什么要使用aliasedMatch.params覆盖location.params属性呢？
             * 示例：
             * {
             *      path:'/a/:hello',
             *      alias:'/b/:world/:hello'
             * }
             *
             * 访问的url为：'/b/go/fighting'，
             * 则此时location.params为：{hello:'fighting', world:'go'}
             * aliasedMatch.params的结果为：{hello:'fighting'}，
             * 所以需要使用正确的params参数覆盖之前错误的params参数。
             * 需要注意一点，如果是alias:'/b/:world'，则是错误的，因为需要一个值为hello的参数，
             * 在当前方法的第一行获得aliasedPath变量的值的fillParams方法中进行了检测。
             */
            location.params = aliasedMatch.params
            return _createRoute(aliasedRecord, location)
        }
        return _createRoute(null, location)
    }

    /**
     * @param record 根据location获得的RouteRecord对象
     * @param location 跳转的信息
     * @param redirectedFrom 由源码可知，如果存在多级的重定向，则redirectFrom仍然是
     *      最初的那个Location对象，
     *      示例：
     *      '/a' --> '/b' --> '/c'
     *      意思是说，'/a'重定向到'/b'，'/b'又重定向到'/c'，在访问'/a'链接时，会创建一个Location对象，
     *      最终找到'/c'的RouteRecord之后，redirectedFrom参数仍然为最初'/a'创建的那个Location对象
     * @returns {Route}
     * @private
     */
    function _createRoute(record: ?RouteRecord,
                          location: Location,
                          redirectedFrom?: Location): Route {
        if (record && record.redirect) {
            return redirect(record, redirectedFrom || location)
        }
        if (record && record.matchAs) {
            return alias(record, location, record.matchAs)
        }
        return createRoute(record, location, redirectedFrom, router)
    }

    return {
        match,
        addRoutes
    }
}

function matchRoute(regex: RouteRegExp,
                    path: string,
                    params: Object): boolean {
    const m = path.match(regex)

    if (!m) {
        return false
    } else if (!params) {
        return true
    }

    /**
     * 示例：
     * Route的path为：'/a/:hello/:world'
     * path为'/a/1/2'
     * 则regex.keys数组中的对象为：[{
            asterisk:false,
            delimiter:"/",
            name:"hello",
            optional:false,
            partial:false,
            pattern:"[^\/]+?",
            prefix:"/",
            repeat:false
        }, {
            asterisk:false,
            delimiter: "/",
            name:"world",
            optional:false,
            partial:false,
            pattern:"[^\/]+?",
            prefix:"/",
            repeat:false
        }]
     * 所以最后params的结果为：{hello:'1', world:'2'}
     */
    for (let i = 1, len = m.length; i < len; ++i) {
        const key = regex.keys[i - 1]
        const val = typeof m[i] === 'string' ? decodeURIComponent(m[i]) : m[i]
        if (key) {
            params[key.name] = val
        }
    }

    return true
}

function resolveRecordPath(path: string, record: RouteRecord): string {
    return resolvePath(path, record.parent ? record.parent.path : '/', true)
}
